/**
 * @class 轨道物实体
 * @author DuskyHuang 圣子
 * @description 根据开普勒三大定律计算的天体实时绕行轨迹，可在编辑器实时调整数据并观察。
 */

import { attenuate, calcSphereScreenPercent } from 'algorithms';
import { _decorator, Component, Node, Vec3, Mat4, Camera, Color } from 'cc';
import { core, Notifications, utils } from 'cfw';
import { DEG_PERIGON, LAYER_MAIN_3D } from 'const';
import { createNodeAt, setLabelStyle } from 'helpers';
import { constStars, SolarProxy, timer, ui, UIProxy, visit, VisitProxy } from 'proxies';
import { ICelestial } from '../celestial';
import { CelestialLabel } from '../ui/items/celestial-label';
import { CustomizedLOD } from '../entities/customized-lod'
import { Sun } from './sun';
const { tip, group, serialize } = utils;
const { ccclass } = _decorator;
const rotatable: Validator = function (this: Orbiter) { return this.rotatable; }

@ccclass('cocosmos.Orbiter')
export abstract class Orbiter extends CustomizedLOD implements ICelestial {

    // 继承类需要实现：
    public abstract readonly config: celestialConfig;   // 天体配置
    protected abstract readonly rotatable: boolean;     // 是否自旋
    protected abstract configure(): void;               // 首次配置
    protected abstract readonly important: boolean;     // 重要天体
    public abstract cat: into;                          // 天体类型
    public alpha: bit8 = 0;

    public get nearest(): pow10 { return this._nearest + this.bias; }
    public get suggest(): pow10 { return this.nearest + SUGGEST + this.bias; }
    public get panorama(): pow10 { return this._panorama; }
    public get bias(): pow10 { return this._bias; }
    public get rotator(): Node | null { return this._rotator; }

    @group("公转参数.A", "轨道半径", ["unit", "au"])
    public set orbitalRadius(arg: au) { this._r = arg; this._updateOrbitData(); }
    public get orbitalRadius(): au { return this._r; }
    @group("公转参数.A", "公转角速度", ["unit", "deg/s"]) public orbitalVelocity: num.deg = 0;
    @group("公转参数.A", "升交点黄经", ["unit", "deg"])
    public set longitudeOfNode(arg: num.deg) { this._lon = arg; this._updateOrbitData(); }
    public get longitudeOfNode(): num.deg { return this._lon; }
    @group("公转参数.A", "初始春分角", ["unit", "deg"])
    public set orbitalInitAngle(arg: num.deg) { this._oia = arg; this._updateOrbitData(); }
    public get orbitalInitAngle(): num.deg { return this._oia; }
    @group("公转参数.A", "轨道离心率", [0, 1, 0.01])
    public set eccentricity(arg: num.pct) { this._e = arg; this._updateOrbitData(); }
    public get eccentricity(): num.pct { return this._e; }
    @group("公转参数.A", "轨道倾斜角", ["unit", "deg"])
    public set orbitalObliquity(arg: num.deg) { this._obo = arg; this._updateOrbitData(); }
    public get orbitalObliquity(): num.deg { return this._obo; }
    public get ba(): num.pct { return this._ba; }
    public get a(): num.pct { return 2 * this.orbitalRadius / (1 + this._ba); }
    public get b(): num.pct { return this.a * this._ba; }
    public get c(): num.pct { return this.a * this._e; }
    public get orbitMat(): Mat4 { return this._orbitMat; }
    public get orbitAngle(): num.deg { return this._orbitalAngle; }

    @group("自转参数.A", "自转角速度", rotatable, ["unit", "deg/s"]) public rotateVelocity: num.deg = 0;
    @group("自转参数.A", "北极赤经", rotatable, ["unit", "deg"])
    public set northPoleRightAscension(arg: num.deg) { this._nra = arg; this._updateRotateData(); }
    public get northPoleRightAscension(): num.deg { return this._nra; }
    @group("自转参数.A", "初始黎明角", rotatable, ["unit", "deg"])
    public set rotateInitAngle(arg: num.deg) { this._ria = arg; this._updateRotateData(); }
    public get rotateInitAngle(): num.deg { return this._ria; }
    @group("自转参数.A", "转轴倾斜角", rotatable, ["unit", "deg"])
    public set rotateObliquity(arg: num.deg) { this._rto = arg; this._updateRotateData(); }
    public get rotateObliquity(): num.deg { return this._rto; }

    @group("体积参数.A", "赤道半径", ["unit", "au"]) public equatorialRadius: au = 1;

    public get far(): number { return Vec3.distance(this.node.worldPosition, this._viewer.node.worldPosition); }
    public get dist(): number { return Vec3.distance(this.node.worldPosition, this._belong.node.worldPosition); }
    public get distSqr(): number { return Vec3.squaredDistance(this.node.worldPosition, this._belong.node.worldPosition); }
    public get belong(): ICelestial { return this._belong; }

    public loadConfig(config: celestialConfig, viewer: Camera, base: pow10, belong: ICelestial): this {
        this.node.name = config.name;
        this._config = config;
        this._belong = belong;
        this._sun = belong.cat === 'star' ? belong : belong.belong;
        this._viewer = viewer;
        this.configure();
        this.initPhysicsParams(base);
        return this;
    }

    protected init(): void {
        if (this.rotatable) this._rotator = this.node.getChildByName('Rotator') ?? null;
    }

    protected onDisable(): void { this._freeLabel(); }

    protected start(): void {
        this._updateOrbitData();
        this.rotatable && this._updateRotateData();
    }

    protected onTick(dt?: time.sec, tick?: num.int): void {
        const span = dt * timer.timescale;
        this.step(span);
        this.rotatable && this.rotate(span);
        this.revolve(span);
        this.syncActivating();
        // this.syncLODChoosing();
        this.syncInstancing();
        this.syncNameLabel();
    }

    protected step(span: time.sec): void {
        // 开普勒第二定律（面积定律）velocity为正 => 逆时针旋转
        const rat = this._r / Vec3.distance(this.node.position, Vec3.ZERO);
        this._orbitalAngle += this.orbitalVelocity * rat * span;
        this._rotateAngle += this.rotateVelocity * span;
        if (this._orbitalAngle > DEG_PERIGON) this._orbitalAngle -= DEG_PERIGON;
        if (this._rotateAngle > DEG_PERIGON) this._rotateAngle -= DEG_PERIGON;
    }

    protected rotate(span?: time.sec): void {
        this._rotator?.active && this._rotator.children.forEach(child => {
            child.active && child.setRotationFromEuler(0, this._rotateAngle, 0);
        });
    }

    protected revolve(span?: time.sec): void {
        const { _r } = this, rad = Math.parseRadian(this._orbitalAngle);
        const [cosA, sinA] = [Math.cos(rad), Math.sin(rad)];
        v3a.set(_r * sinA, 0, _r * cosA);
        this.node.setPosition(Vec3.transformMat4(v3a, v3a, this._orbitMat));
    }

    protected syncActivating(): void {
        if (!this.node.parent || !this.node.parent.activeInHierarchy) return;
        if (this.cat === 'satellite') return; // TEMP TODO
        const activated = this.distSqr > (this.important ? CUL_VAV_SQR : NOT_IMP_CUL_VAV_SQR);
        this.node.active = activated;
    }

    protected syncInstancing(): void { }

    protected syncNameLabel(): void {
        const { showCelNames } = ui;
        if (this.node.activeInHierarchy && showCelNames) {
            if (!this._label) {
                if (visit.inSolar) {
                    this._label = setLabelStyle(CelestialLabel.claim('solar', this.node.name), 'solar', this.cat as Exclude<orbit, 'satellite' | 'asteroid'>);
                } else {
                    this._label = setLabelStyle(CelestialLabel.claim('planetary', this.node.name), 'planetary', this.cat as Extract<orbit, 'planet' | 'satellite'>);
                }
                this._label.camera = this._viewer;
                this._label.target = this.node;
                this._label.key = `celestial@${this._config.name}`;
            }
            this._label.alpha = attenuate(this.distSqr, NameLabelAtt.heads(2) as vec2<number>, true);
        } else this._freeLabel();
    }

    private _freeLabel(): void {
        if (this._label) {
            this._label.free();
            this._label = null;
        }
    }

    protected initPhysicsParams(base: pow10): void {
        const { diam } = this._config;
        this._real.set(diam, diam, diam);
        this.updateScale();
        const { orbital, rotate } = this._config.orbiter!;
        const { radius: R = 1, lon: L = 0, eccentricity: E = 0, obliquity: O = 0, period: P = 365.25, angle: I = 0 } = orbital!;
        this.orbitalRadius = R;
        this.orbitalVelocity = 1 / P;
        this.longitudeOfNode = L;
        this.eccentricity = E;
        this.orbitalObliquity = O;
        this.orbitalInitAngle = I;
        const { nra: n = 0, obliquity: o = 0, period: p = 1, angle: a = 0 } = rotate ?? { nra: 0, obliquity: 0, period: 1, angle: 0 };
        this.northPoleRightAscension = n;
        this.rotateInitAngle = a;
        if (p === 'sync') this.rotateVelocity = this.orbitalVelocity;
        else this.rotateVelocity = 1 / p;
        this.rotateObliquity = o;
        this._nearest = Math.log10(diam / 2) + base + BASE;
        this._panorama = Math.log10(R * 0.8) + base;
    }

    protected _updateRotateData(): void {
        this._rotator && this._rotator.setRotationFromEuler(v3a.set(0, this._nra, -this._rto));
        this._rotateAngle = this._ria;
    }

    protected _updateOrbitData(): void {
        this._orbitalAngle = this._oia;
        this._ba = Math.sqrt(1 - this._e * this._e) + Number.EPSILON;
        this._a = 2 * this._r / (1 + this._ba);
        this._b = this._a * this._ba;
        this._c = this._a * this._e;
        // 开普勒第一定律（轨道定律）
        Mat4.fromYRotation(this._orbitMat, Math.parseRadian(this._lon));
        Mat4.rotateZ(this._orbitMat, this._orbitMat, Math.parseRadian(this._obo)).
            transform(v3a.set(this._c, 0, 0))
            .scale(v3a.set(this._a / this._r, 1, this._b / this._r));
        this.revolve();
    }

    public faceToSun(child: Node): void {
        child.forward = Vec3.subtract(v3a, this.node.worldPosition, this._sun.node.worldPosition);
    }

    protected _rotator!: Node;
    protected _orbitalAngle: num.deg = 0;
    protected _rotateAngle: num.deg = 0;
    @serialize protected _r: au = 1;
    @serialize protected _lon: num.deg = 0;
    @serialize protected _oia: num.deg = 0;
    @serialize protected _e: number = 0;
    @serialize protected _obo: num.deg = 0;
    @serialize protected _nra: num.deg = 0;
    @serialize protected _rto: num.deg = 0;
    @serialize protected _ria: num.deg = 0;
    protected _a: number = 1;
    protected _b: number = 1;
    protected _c: number = 0;
    protected _ba: number = 1;
    protected _orbitMat = new Mat4;
    protected _belong: ICelestial;
    protected _sun: Sun;
    protected _config: celestialConfig;
    protected _bias: pow10 = 0;
    private _label: CelestialLabel = null;
    private _nearest: pow10 = 0;
    private _panorama: pow10 = 0;
}


const { WPS_CULLING_VALVE, NameLabelAtt } = UIProxy;
const { LensOffsets: { BASE, SUGGEST } } = VisitProxy;
const CUL_VAV_SQR = WPS_CULLING_VALVE * WPS_CULLING_VALVE;
const NOT_IMP_CUL_VAV_SQR = CUL_VAV_SQR * 10;

const v3a = new Vec3;